feat: add global server power control mutations#1970
Conversation
New GraphQL mutations for server reboot and shutdown, replacing the legacy form-based Boot.php approach. Gated behind UPDATE_ANY CONFIG permissions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace legacy form POST to Boot.php with proper GraphQL mutations. Adds noRetry context to prevent Apollo RetryLink from re-triggering power actions when the connection drops during reboot/shutdown.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughThis pull request adds GraphQL mutations for server power control (reboot and shutdown), implements NestJS resolvers and a service that execute system power commands, extends the GraphQL ArrayStateInput with decryption credentials, migrates the web client's internal boot handler from HTML form submission to GraphQL mutations, and updates generated types and the API version. Changes
Sequence DiagramsequenceDiagram
participant Client as Web Client
participant GraphQL as GraphQL API
participant Resolver as ServerPowerMutationsResolver
participant Service as ServerPowerService
participant System as System (reboot/poweroff)
Client->>GraphQL: mutation ServerReboot / ServerShutdown
GraphQL->>Resolver: resolve field (permission check)
Resolver->>Service: call reboot() / shutdown()
Service->>System: execa('/sbin/reboot' or '/sbin/poweroff', ['-n'])
System-->>Service: command result
Service-->>Resolver: returns Promise<boolean>
Resolver-->>GraphQL: field result
GraphQL-->>Client: { data: { serverPower: { reboot|shutdown: true } } }
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a910a6f35
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export const submitInternalBootReboot = async () => { | ||
| const apolloClient = useApolloClient().client; | ||
| await apolloClient.mutate({ |
There was a problem hiding this comment.
Handle power-mutation promise failures in helper
These helpers now return rejected promises on GraphQL/network errors, but the existing callers still invoke them fire-and-forget (OnboardingNextStepsStep.vue and OnboardingInternalBoot.standalone.vue), so a failed reboot/shutdown request becomes an unhandled rejection and leaves the onboarding flow in a non-recoverable loading path instead of surfacing an error or fallback. Please either swallow/normalize expected disconnect errors inside these helpers or make callers await and handle failures.
Useful? React with 👍 / 👎.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1970 +/- ##
==========================================
- Coverage 52.12% 52.10% -0.03%
==========================================
Files 1031 1034 +3
Lines 71589 71624 +35
Branches 8102 8096 -6
==========================================
Hits 37319 37319
- Misses 34145 34180 +35
Partials 125 125 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts (1)
9-19: Consider wrappingexecacalls in try/catch for consistent error handling.The existing
ups.service.tspattern wrapsexecacalls with error handling to avoid exposing raw OS-level error messages to GraphQL clients. If these commands fail (e.g., permission denied, binary not found), the current implementation would propagate the raw error.♻️ Suggested error handling pattern (based on ups.service.ts)
async reboot(): Promise<boolean> { this.logger.log('Server reboot requested via GraphQL'); - await execa('/sbin/reboot', ['-n']); - return true; + try { + await execa('/sbin/reboot', ['-n']); + return true; + } catch (error) { + this.logger.error('Failed to initiate server reboot:', error); + throw new Error( + `Failed to initiate server reboot: ${error instanceof Error ? error.message : String(error)}` + ); + } } async shutdown(): Promise<boolean> { this.logger.log('Server shutdown requested via GraphQL'); - await execa('/sbin/poweroff', ['-n']); - return true; + try { + await execa('/sbin/poweroff', ['-n']); + return true; + } catch (error) { + this.logger.error('Failed to initiate server shutdown:', error); + throw new Error( + `Failed to initiate server shutdown: ${error instanceof Error ? error.message : String(error)}` + ); + } }Based on learnings: "In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts` around lines 9 - 19, The reboot() and shutdown() methods call execa directly and can throw raw OS errors to GraphQL clients; wrap the execa('/sbin/reboot', ['-n']) in reboot() and execa('/sbin/poweroff', ['-n']) in shutdown() with try/catch, log the error via this.logger.error and throw a controlled error (or return false) consistent with the pattern used in ups.service.ts so the service handles failures instead of exposing raw exceptions to resolvers; ensure you reference the reboot and shutdown methods and follow the same error message format and return semantics as ups.service.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/components/Onboarding/composables/internalBoot.ts`:
- Around line 231-247: The helpers submitInternalBootReboot and
submitInternalBootShutdown currently expose rejected Promises when the server
drops connection; either require callers to await them or make them truly
fire-and-forget—change the functions (submitInternalBootReboot and
submitInternalBootShutdown) to wrap the apolloClient.mutate call in a try/catch
and swallow or log errors (e.g., process/console logging) so the returned
Promise never rejects, preserving the fire-and-forget contract used by
OnboardingNextStepsStep.vue and OnboardingInternalBoot.standalone.vue.
---
Nitpick comments:
In `@api/src/unraid-api/graph/resolvers/server-power/server-power.service.ts`:
- Around line 9-19: The reboot() and shutdown() methods call execa directly and
can throw raw OS errors to GraphQL clients; wrap the execa('/sbin/reboot',
['-n']) in reboot() and execa('/sbin/poweroff', ['-n']) in shutdown() with
try/catch, log the error via this.logger.error and throw a controlled error (or
return false) consistent with the pattern used in ups.service.ts so the service
handles failures instead of exposing raw exceptions to resolvers; ensure you
reference the reboot and shutdown methods and follow the same error message
format and return semantics as ups.service.ts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a00898ec-f07f-40f8-9cc3-66a6ef6ab9fb
⛔ Files ignored due to path filters (1)
api/src/unraid-api/cli/generated/graphql.tsis excluded by!**/generated/**
📒 Files selected for processing (14)
AGENTS.mdapi/dev/configs/api.jsonapi/generated-schema.graphqlapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/resolvers.module.tsapi/src/unraid-api/graph/resolvers/server-power/server-power.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/server-power/server-power.service.spec.tsapi/src/unraid-api/graph/resolvers/server-power/server-power.service.tsweb/__test__/components/Onboarding/internalBoot.test.tsweb/src/components/Onboarding/composables/internalBoot.tsweb/src/components/Onboarding/graphql/serverPower.mutation.tsweb/src/composables/gql/gql.tsweb/src/composables/gql/graphql.ts
💤 Files with no reviewable changes (1)
- AGENTS.md
| export const submitInternalBootReboot = async () => { | ||
| const apolloClient = useApolloClient().client; | ||
| await apolloClient.mutate({ | ||
| mutation: SERVER_REBOOT_MUTATION, | ||
| fetchPolicy: 'no-cache', | ||
| context: { noRetry: true }, | ||
| }); | ||
| }; | ||
|
|
||
| export const submitInternalBootReboot = () => submitBootCommand('reboot'); | ||
| export const submitInternalBootShutdown = () => submitBootCommand('shutdown'); | ||
| export const submitInternalBootShutdown = async () => { | ||
| const apolloClient = useApolloClient().client; | ||
| await apolloClient.mutate({ | ||
| mutation: SERVER_SHUTDOWN_MUTATION, | ||
| fetchPolicy: 'no-cache', | ||
| context: { noRetry: true }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C2 '\bsubmitInternalBoot(Reboot|Shutdown)\s*\(' web/src/components/OnboardingRepository: unraid/api
Length of output: 1615
🏁 Script executed:
#!/bin/bash
# Check the actual functions at lines 231-247 to confirm they are async
sed -n '225,255p' web/src/components/Onboarding/composables/internalBoot.tsRepository: unraid/api
Length of output: 606
🏁 Script executed:
#!/bin/bash
# Check the caller contexts in both Vue files for error handling or other patterns
echo "=== OnboardingNextStepsStep.vue context ==="
sed -n '125,150p' web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
echo -e "\n=== OnboardingInternalBoot.standalone.vue context ==="
sed -n '55,75p' web/src/components/Onboarding/standalone/OnboardingInternalBoot.standalone.vueRepository: unraid/api
Length of output: 1353
Keep these power helpers fire-and-forget, or await them everywhere.
noRetry avoids duplicate mutations, but a successful reboot/shutdown can still make Apollo reject once the server drops the connection before sending a response. These helpers now return a Promise from async functions, while all current callers in OnboardingNextStepsStep.vue (lines 135, 140) and OnboardingInternalBoot.standalone.vue (lines 64, 66) invoke them without await. If the mutation fails, the rejection goes unhandled and the onboarding flow has no recovery path.
One way to preserve the previous fire-and-forget contract
-export const submitInternalBootReboot = async () => {
+export const submitInternalBootReboot = (): void => {
const apolloClient = useApolloClient().client;
- await apolloClient.mutate({
- mutation: SERVER_REBOOT_MUTATION,
- fetchPolicy: 'no-cache',
- context: { noRetry: true },
- });
+ void apolloClient
+ .mutate({
+ mutation: SERVER_REBOOT_MUTATION,
+ fetchPolicy: 'no-cache',
+ context: { noRetry: true },
+ })
+ .catch(() => undefined);
};
-export const submitInternalBootShutdown = async () => {
+export const submitInternalBootShutdown = (): void => {
const apolloClient = useApolloClient().client;
- await apolloClient.mutate({
- mutation: SERVER_SHUTDOWN_MUTATION,
- fetchPolicy: 'no-cache',
- context: { noRetry: true },
- });
+ void apolloClient
+ .mutate({
+ mutation: SERVER_SHUTDOWN_MUTATION,
+ fetchPolicy: 'no-cache',
+ context: { noRetry: true },
+ })
+ .catch(() => undefined);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/components/Onboarding/composables/internalBoot.ts` around lines 231 -
247, The helpers submitInternalBootReboot and submitInternalBootShutdown
currently expose rejected Promises when the server drops connection; either
require callers to await them or make them truly fire-and-forget—change the
functions (submitInternalBootReboot and submitInternalBootShutdown) to wrap the
apolloClient.mutate call in a try/catch and swallow or log errors (e.g.,
process/console logging) so the returned Promise never rejects, preserving the
fire-and-forget contract used by OnboardingNextStepsStep.vue and
OnboardingInternalBoot.standalone.vue.
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |
Summary
Adds GraphQL mutations for server reboot and shutdown, replacing the legacy form-based Boot.php approach. These mutations are now available globally (not scoped to onboarding), gated behind
UPDATE_ANY CONFIGpermissions.Server Power API:
ServerPowerServicewithreboot()andshutdown()methods calling/sbin/reboot -nand/sbin/poweroff -nServerPowerMutationsResolveras nested resolver underMutation.serverPowerresolvers.module.tswith proper NestJS DIFrontend (Onboarding):
/plugins/dynamix/include/Boot.phpwith proper GraphQL mutationsnoRetrycontext to prevent Apollo RetryLink from re-triggering power actions when the connection drops during reboot/shutdownserverPower.mutation.tswithServerRebootandServerShutdownmutationsHousekeeping:
Test Coverage
All new code paths have test coverage:
server-power.service.spec.ts: 4 tests (happy path + error for reboot/shutdown)internalBoot.test.ts: 12 tests covering reboot, shutdown, create, BIOS parsing, error pathsnoRetrycontext is passed to prevent retry-triggered power actionsPre-Landing Review
Codex structured review found 1 P1 (Apollo RetryLink retrying power mutations). Fixed by adding
context: { noRetry: true }to both mutation calls.Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores